7

前言

自动化打包工具webpack,相信很多人和我一样尝试着研究下它,但是繁杂的功能以及高度抽象的代码实在是很难理解,所以笔者只能通过github的webpack的第一次提交进行分析,实现,并将实现的一些心得分享一下。

功能分析

对于node端来讲,有commonjs来规范模块的标识,定义,引用。而浏览器端由于缺乏原生对象支持就需要通过自我实现来模拟commonjs规范。
webpack是通过一个IIFE立即调用函数表达式去实现这个规范的。简要的去注释,去除内部运行的代码,其格式如下:

(function(module){})([function(){},function(){}]);

简单点说就是各个模块代码以数组的形式传递给运行函数,在进行存储。详细分析可以参考简要分析webpack打包后代码

所以实现以上的功能需求点如下:

  • 文件路径分析与定位resolve
  • 文件编译&解析,分析出依赖文件parse
  • 生成需要打包的文件树depTree
  • 将依赖文件写入输出文件内writeChunk

文件分析与定位

本功能和node的require类似,故有参考node require源码

文件分析,将文件为两种类型

  • 以 "./","../","/" 标识符开头的路径文件模块

    • 该类文件会通过path.join 转化为真实的路径而定位。
  • 自定义的文件模块

    • 这类相对比较麻烦,他在当前目录下面的node_modules,查找文件,未找到则一路向上查找,最终查找到或者抛出异常。如:
[ '/Users/zhujian/Documents/workspace/webpack/simple-webpack/node_modules',
  '/Users/zhujian/Documents/workspace/webpack/node_modules',
  '/Users/zhujian/Documents/workspace/node_modules',
  '/Users/zhujian/Documents/node_modules',
  '/Users/zhujian/node_modules',
  '/Users/node_modules',
  '/node_modules' ]

文件定位

  • 对于部分文件并没有带扩展名,此时有默认的扩展名依次以.js,.jsx为后缀依次补充。当然我们可以用传入extensions,修改默认的扩展名。
{extensions:['js','jsx','jpg']}
  • 当发现该路径为文件夹时则,则依次查找如下文件

    • package.json(main字段)
    • index+(扩展名)

文件解析

  • 文件可以定位之后,则是解析定位下来的文件了,本文用的是exprima,文档如parser规范文档

    • esprima解析文件,返回一个语法树。
    • 对语法树进行遍历,对遇到type 为CallExpression,且其callee为name为require的节点,将该节点的value,以及下标包装成对象储存起来。

比如

const b = require('./b');

解析后

....
"init": {
        "type": "CallExpression",
        "callee": {
            "type": "Identifier",
            "name": "require",
            "range": [
                10,
                17
            ]
        },
        "arguments": [
            {
                "type": "Literal",
                "value": "./b",
                "raw": "'./b'",
                "range": [
                    18,
                    23
                ]
            }
        ],
        "range": [
            10,
            24
        ]
    },
 ....   

我们要做的就是提取value "./b",以及该字符串在文件所处的位置range。

文件树生成

主要是从入口文件开始,将所有依赖的js,以及其内容,分配的id组成一个可操作的扁平化的对象和存储着name与id对应的map对象。

实现手法上也是递归resolve函数,获取到各个文件的依赖,文件,id的信息,最后得到depTree对象

举个例子:

{ modules: 
   { '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js': 
      { filename: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js',
        id: 0,
        requires: [Array],
        rangeRequires: [Array],
        source: 'const b = require(\'./b\');\nconst c = require(\'c\');\nconst {e, f, g} = require(\'./m\');\n\n 
      },
     '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/b.js': 
      { filename: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/b.js',
        id: 1,
        requires: [],
        rangeRequires: [],
        source: 'const b = \'b\';\n\nmodule.exports = b;\n' 
      },
     '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/node_modules/c.js': 
      { filename: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/node_modules/c.js',
        id: 2,
        requires: [],
        rangeRequires: [],
        source: 'const c = \'c\';\n\nmodule.exports = c;\n' 
      },
     '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/m.js': 
      { filename: '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/m.js',
        id: 3,
        requires: [],
        rangeRequires: [],
        source: '// const core = require(\'./core\');\nconst a = 1;\n\n
      },
  nextModuleId: 4,
  mapNameToId: 
   { '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js': 0,
     '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/b.js': 1,
     '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/node_modules/c.js': 2,
     '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/m.js': 3 
     } 
 }

文件写入

写入主函数,替换入口的执行函数。这块会用到之前的path和id关系的map对象,通过路口文件的绝对路径,找出入口文件的mainId,并进行替换。

写入参数数组。遍历文件树,将文件节点的source内容替换掉

大致如下:

require('module') 替换为__webpack_require__(0)
  • 这个地方要考虑的点是

    • 如果用replace替换的话,会影响source带部分关键字的内容,不可取。
    • 用索引替换的字符串的话,一旦第一个替换成功,整个字符串长度发生变化,原先的索引下标就失效了。

官方实现

     const result = [source];
    replaces.forEach(replace => {
        const {from, value, end} = replace;
        const source = result.shift();
        result.unshift(source.substr(0, from), value, source.substr(end))
    });

代码实现

本人的简易版webpack实现simple-webpack

(完)

参考资料

  1. webpack早期源码
  2. require源码解读
  3. node require源码
  4. parser规范文档
  5. 简要分析webpack打包后代码

朱建
400 声望70 粉丝

有空的时候给自己设定一个小目标。